package de.fhin.mas.agents;

import jade.content.ContentElement;
import jade.content.lang.Codec;
import jade.content.lang.Codec.CodecException;
import jade.content.lang.sl.SLCodec;
import jade.content.onto.Ontology;
import jade.content.onto.OntologyException;
import jade.core.AID;
import jade.core.Agent;
import jade.core.ServiceException;
import jade.core.messaging.TopicManagementHelper;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.lang.acl.ACLMessage;
import jade.util.Logger;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.logging.Level;

import de.fhin.mas.MASEvents;
import de.fhin.mas.MASServices;
import de.fhin.mas.ontology.EnvironmentData;
import de.fhin.mas.ontology.EnvironmentScanResult;
import de.fhin.mas.ontology.Location;
import de.fhin.mas.ontology.LocationData;
import de.fhin.mas.ontology.MASOntology;
import de.fhin.mas.ontology.SearchableObject;
import de.fhin.mas.ontology.SearchableObjectData;
import de.fhin.mas.ontology.TargetData;
import de.fhin.mas.ontology.TargetSearchResult;
import de.fhin.mas.util.ArgumentCheck;

/**
 * Basis-Klasse für alle Agenten des MAS-Projekts.
 * 
 * @author Ast
 * @author Alexander Weickmann
 * @author Thomas Schlaefer
 */
public abstract class MASBaseAgent extends Agent {

	// ************************************************************
	// *** ontologierelevantes Zeug hier drin: ***
	private final Codec codec = new SLCodec();
	private final Ontology ontology = MASOntology.getInstance();
	// ************************************************************

	/** Die eindeutige ID zur Serialisierung über das Netzwerk. */
	private static final long serialVersionUID = 7984262481114110610L;

	/** Der Logger des Agenten. */
	private final Logger logger;

	/** Helper zum Handling der Topics. */
	private TopicManagementHelper topicHelper;

	/** Topics zu denen der Agent registriert ist. */
	private final HashMap<MASEvents, AID> subscribedTopics;

	/** Der von dem Agenten angebotene Service. */
	private final MASServices service;

	private int currentConversationId;

	/**
	 * Erzeugt einen <tt>MASBaseAgent</tt>.
	 * 
	 * @param service
	 *            Der Service, der von dem neuen Agenten angeboten wird (kann
	 *            momentan nur einer sein).
	 * 
	 * @throws NullPointerException
	 *             Falls <tt>service</tt> <tt>null</tt> ist.
	 */
	public MASBaseAgent(MASServices service) {
		ArgumentCheck.notNull(service);

		this.service = service;
		this.logger = Logger.getMyLogger(getClass().getName());
		this.subscribedTopics = new HashMap<MASEvents, AID>();
		this.currentConversationId = Integer.MIN_VALUE;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void setup() {

		registerWithPlatform();
		initTopicHelper();
		subscribeToTopics();
		addBehaviours();

		// ************************************************************
		// *** ontologierelevantes Zeug hier drin: ***
		getContentManager().registerLanguage(codec);
		getContentManager().registerOntology(ontology);
		// ************************************************************

		logger.log(Level.INFO, "Ready.");

	}

	/** Registriert den Agenten bei der Agentenplattform. */
	private void registerWithPlatform() {
		ServiceDescription sd = new ServiceDescription();
		sd.setType(service.getName());
		sd.setName(getName());
		sd.setOwnership("MAS");

		DFAgentDescription dfd = new DFAgentDescription();
		dfd.addServices(sd);
		dfd.setName(getAID());

		try {
			DFService.register(this, dfd);
		} catch (FIPAException e) {
			logger.log(Level.INFO, "Registration with agent platform failed.", e);
			doDelete();
		}
		logger.log(Level.INFO, "Succeeded in registration with agent platform.");
	}

	/**
	 * ACHTUNG: Zusätzlicher Start-Parameter
	 * "-services jade.core.messaging.TopicManagementService" notwendig, damit
	 * Initialisierung klappt!
	 * 
	 */
	private void initTopicHelper() {
		try {
			topicHelper = (TopicManagementHelper) getHelper(TopicManagementHelper.SERVICE_NAME);
		} catch (ServiceException e) {
			logger.log(Logger.WARNING, "Initializing the TopicManagementHelper failed!", e);
			logger.log(Logger.WARNING,
					"Please add start argument ''-services jade.core.messaging.TopicManagementService''!");
			doDelete();
		}
	}

	/**
	 * Alias für subscribeToTopic.
	 * <p>
	 * Ein Holen der Subscription registriert das Topic implizit, falls noch
	 * nicht geschehen!
	 * 
	 * @see subscribeToTopic
	 */
	public AID getSubscription(MASEvents topic) {
		return subscribeToTopic(topic);
	}

	/**
	 * Registriere den Agenten für ein bestimmtes Topic bei der Plattform.
	 * 
	 * @param topic
	 *            Das Topic für welches der Agent registriert werden soll.
	 * @return Objekt, das in Message-Templates zum Filtern auf Topic verwendet
	 *         werden kann.
	 */
	public AID subscribeToTopic(MASEvents topic) {
		AID result = subscribedTopics.get(topic);
		if (result == null) {
			result = getTopicHelper().createTopic(topic.getName());
			subscribedTopics.put(topic, result);
		}
		return result;
	}

	/**
	 * Registriere Agenten zum Empfang der für ihn interessanten Message-Topics.
	 */
	protected void subscribeToTopics() {
		if (getTopicHelper() != null) {
			subscribeToTopic(MASEvents.ABORT_AND_GO_TO);
			subscribeToTopic(MASEvents.RESUME);
			subscribeToTopic(MASEvents.SYSTEM_STOP_REQUIRED);

			this.addTopicSubsciptions();
		}
	}

	/**
	 * Registrierung des spezifischen Agenten für die aus seiner sicht
	 * zusätzlich nötigen Topics.
	 * <p>
	 * ACHTUNG: Sowohl Topics, die er empfangen will, als auch Topics, die mit
	 * denen er versendet!
	 */
	protected abstract void addTopicSubsciptions();

	/** Fügt dem Agenten seine Behaviours hinzu. */
	protected abstract void addBehaviours();

	/**
	 * Fügt der übergebenen <tt>ACLMessage</tt> eine im MAS eindeutige
	 * <tt>conversation-ID</tt> hinzu.
	 * 
	 * @param msg
	 *            Die <tt>ACLMessage</tt> der eine eindeutige
	 *            <tt>conversation-ID</tt> hinzugefügt werden soll.
	 * 
	 * @throws NullPointerException
	 *             Falls <tt>msg</tt> <tt>null</tt> ist.
	 */
	public void setConversationId(ACLMessage msg) {
		ArgumentCheck.notNull(msg);

		if (currentConversationId > Integer.MAX_VALUE - 1) {
			currentConversationId = Integer.MIN_VALUE;
		}
		msg.setConversationId(getName() + '_' + currentConversationId);
	}

	/**
	 * Alle anderen Agenten als Empfänger der gegebenen <tt>ACLMessage</tt>
	 * setzen.
	 */
	public void setAllAsReceivers(ACLMessage msg) {
		ArgumentCheck.notNull(msg);

		// Bisherige Empfänger leeren
		msg.clearAllReceiver();

		// Alle Services suchen und als Empfänger setzen
		try {
			Set<AID> all = MASServices.findAllServices(this);
			for (AID currentService : all) {
				msg.addReceiver(currentService);
			}
		} catch (FIPAException e) {
			logger.log(Level.SEVERE, "An exception occured while searching for other agents.", e);
		}
	}

	/**
	 * Sucht nach allen aktuell bei der Agentenplattform angemeldeten Agenten
	 * und fügt diese als Empfänger der übergebenen Nachricht hinzu. Dann wird
	 * die Nachricht abgeschickt.
	 */
	public void sendToAll(ACLMessage msg) {
		ArgumentCheck.notNull(msg);
		setAllAsReceivers(msg);
		send(msg);
	}

	/**
	 * Wie <tt>sendToAll(ACLMessage)</tt>, aber nachdem die Nachricht
	 * abgeschickt wurde wird noch eine Statusinfo geloggt, die den Sendevorgang
	 * festhält.
	 * 
	 * @see #sendToAll(ACLMessage)
	 */
	public void sendToAllAndLog(ACLMessage msg) {
		ArgumentCheck.notNull(msg);
		setAllAsReceivers(msg);
		sendAndLog(msg);
	}

	/**
	 * Sendet die gegebene Nachricht, aber nachdem die Nachricht abgeschickt
	 * wurde wird noch eine Statusinfo geloggt, die den Sendevorgang festhält.
	 */
	public void sendAndLog(ACLMessage msg) {
		ArgumentCheck.notNull(msg);

		String receivers = "";
		@SuppressWarnings("unchecked")
		Iterator<AID> it = msg.getAllReceiver();

		while (it.hasNext()) {
			AID currentAid = it.next();
			String currentAgentName = currentAid.getName();
			receivers += (it.hasNext()) ? currentAgentName + ", " : currentAgentName;
		}

		logger.log(Level.INFO, ACLMessage.getPerformative(msg.getPerformative()) + ' ' + msg.getConversationId()
				+ " sent to [" + receivers + "].");
		send(msg);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	protected void takeDown() {
		logger.log(Logger.INFO, "Shutting down.");

		// Von der Plattform abmelden
		try {
			DFService.deregister(this);
		} catch (FIPAException e) {
			logger.log(Logger.SEVERE, "Deregistering agent from the agent platform failed due to an exception.", e);
		}
	}

	/** Gibt den <tt>Logger</tt> des Agenten zurück. */
	public Logger getLogger() {
		return logger;
	}

	public TopicManagementHelper getTopicHelper() {
		return topicHelper;
	}

	/**
	 * Fügt das Suchobjekt in Form eines SL Strings in den Content der
	 * ACLMessage ein
	 * 
	 * @param msg
	 *            Die <tt>ACLMessage</tt>, in die das Suchobjekt eingefügt
	 *            werden soll
	 * @param searchableObject
	 *            Das Suchobjekt selbst in Form eines <tt>SearchableObject</tt>
	 * @param location
	 *            Der Lagerort für das zu suchende Objekt
	 * @return die übergebene
	 *         <tt>ACLMessage mit dem übergebenen searchableObject als Content</tt>
	 */
	public ACLMessage fillMsgContent(ACLMessage msg, SearchableObject searchableObject, Location location) {

		// Content Language und Ontologie der Message setzen:
		msg.setLanguage(codec.getName());
		msg.setOntology(ontology.getName());

		SearchableObjectData data = new SearchableObjectData(searchableObject, location);

		try {
			// JADE konvertiert das SearchableObjectData JavaObjekt in einen SL
			// language String:
			getContentManager().fillContent(msg, data);
		} catch (CodecException ce) {
			ce.printStackTrace();
		} catch (OntologyException oe) {
			oe.printStackTrace();
		}

		return msg;
	}

	/**
	 * Fügt die Location in Form eines SL Strings in den Content der ACLMessage
	 * ein
	 * 
	 * @param msg
	 *            Die <tt>ACLMessage</tt>, in der die StorageLocation eingefügt
	 *            werden soll
	 * @param location
	 *            Der Lagerort für das zu suchende Objekt
	 * @return die übergebene
	 *         <tt>ACLMessage mit dem übergebenen storageLocation als Content</tt>
	 */
	public ACLMessage fillMsgContent(ACLMessage msg, Location location) {

		// Content Language und Ontologie der Message setzen:
		msg.setLanguage(codec.getName());
		msg.setOntology(ontology.getName());

		LocationData data = new LocationData(location);

		try {
			getContentManager().fillContent(msg, data);
		} catch (CodecException ce) {
			ce.printStackTrace();
		} catch (OntologyException oe) {
			oe.printStackTrace();
		}

		return msg;
	}

	/**
	 * Fügt ein Ergebnis der Zielsuche in Form eines SL Strings in den Content
	 * der ACLMessage ein
	 * 
	 * @param msg
	 *            Die <tt>ACLMessage</tt>, in die das Suchergebnis eingefügt
	 *            werden soll
	 * @param targetSearchResult
	 *            Das Suchergebnis selbst in Form eines
	 *            <tt>TargetSearchResult</tt>
	 * @return die übergebene
	 *         <tt>ACLMessage mit dem übergebenen targetSearchResult als Content</tt>
	 */
	public ACLMessage fillMsgContent(ACLMessage msg, TargetSearchResult targetSearchResult) {

		// Content Language und Ontologie der Message setzen:
		msg.setLanguage(codec.getName());
		msg.setOntology(ontology.getName());

		TargetData data = new TargetData(targetSearchResult);

		try {
			// JADE konvertiert das SearchableObjectData JavaObjekt in einen SL
			// language String:
			getContentManager().fillContent(msg, data);
		} catch (CodecException ce) {
			ce.printStackTrace();
		} catch (OntologyException oe) {
			oe.printStackTrace();
		}

		return msg;
	}

	/**
	 * Fügt ein Ergebnis der Umgebungssuche in Form eines SL Strings in den
	 * Content der ACLMessage ein
	 * 
	 * @param msg
	 *            Die <tt>ACLMessage</tt>, in die das Suchergebnis eingefügt
	 *            werden soll
	 * @param environmentScanResult
	 *            Das Suchergebnis selbst in Form eines
	 *            <tt>EnvironmentScanResult</tt>
	 * @return die übergebene
	 *         <tt>ACLMessage mit dem übergebenen environmentScanResult als Content</tt>
	 */
	public ACLMessage fillMsgContent(ACLMessage msg, EnvironmentScanResult environmentScanResult) {

		// Content Language und Ontologie der Message setzen:
		msg.setLanguage(codec.getName());
		msg.setOntology(ontology.getName());

		EnvironmentData data = new EnvironmentData(environmentScanResult);

		try {
			// JADE konvertiert das SearchableObjectData JavaObjekt in einen SL
			// language String:
			getContentManager().fillContent(msg, data);
		} catch (CodecException ce) {
			ce.printStackTrace();
		} catch (OntologyException oe) {
			oe.printStackTrace();
		}

		return msg;
	}

	/**
	 * Konvertiert den Content einer <tt>ACLMessage</tt> in ein
	 * <tt>SearchableObject</tt> zurück
	 * 
	 * @param msg
	 *            Die <tt>ACLMessage</tt> mit dem relevanten Content
	 * @return Ein <tt>SearchableObject</tt>, wenn der Messagecontent
	 *         tatsächlich ein <tt>SearchableObject</tt> war. Falls der Content
	 *         ein anderes Datenformat hatte, wird <b>null</b> zurückgegeben.
	 */
	public SearchableObject extractSearchableObjectFromMsg(ACLMessage msg) {

		// Diese Attribute benötigen wir zum Rückwärtskonvertieren aus dem
		// Content String in eine Java Klasse
		ContentElement contentElement = null;
		SearchableObjectData sod = null;
		SearchableObject searchObject = null;

		try {

			// zunächst aus dem SL String ein allgemeines ContentElement
			// instantiieren
			contentElement = getContentManager().extractContent(msg);
			if (contentElement instanceof SearchableObjectData) {

				// den SearchableObjectData Container casten
				sod = (SearchableObjectData) contentElement;
				// Das SearchableObject aus dem SearchableObjectData Container
				// herausziehen:
				searchObject = sod.getSearchObject();
			} else {
				return null;
			}
		} catch (CodecException ce) {
			ce.printStackTrace();
		} catch (OntologyException oe) {
			oe.printStackTrace();
		}

		return searchObject;
	}

	/**
	 * Konvertiert den Content einer <tt>ACLMessage</tt> in ein
	 * <tt>Location</tt> Objekt zurück
	 * 
	 * @param msg
	 *            Die <tt>ACLMessage</tt> mit dem relevanten Content
	 * @return Ein <tt>Location</tt>, wenn der Messagecontent tatsächlich ein
	 *         <tt>Location</tt> anthalten hat. Falls der Content ein anderes
	 *         Datenformat hatte, wird <b>null</b> zurückgegeben.
	 */
	public Location extractLocationFromMsg(ACLMessage msg) {

		// Diese Attribute benötigen wir zum Rückwärtskonvertieren aus dem
		// Content String in eine Java Klasse
		ContentElement contentElement = null;
		SearchableObjectData sod = null;
		LocationData lod = null;
		Location location = null;

		try {

			// zunächst aus dem SL String ein allgemeines ContentElement
			// instantiieren
			contentElement = getContentManager().extractContent(msg);
			if (contentElement instanceof SearchableObjectData) {
				// den SearchableObjectData Container casten
				sod = (SearchableObjectData) contentElement;
				// Das SearchableObject aus dem SearchableObjectData Container
				// herausziehen:
				location = sod.getLocation();
			} else if (contentElement instanceof LocationData) {
				// den LocationData Container casten
				lod = (LocationData) contentElement;
				// Die Location aus dem LocationData Container
				// herausziehen:
				location = lod.getLocation();
			} else {
				return null;
			}
		} catch (CodecException ce) {
			ce.printStackTrace();
		} catch (OntologyException oe) {
			oe.printStackTrace();
		}

		return location;
	}

	/**
	 * Konvertiert den Content einer <tt>ACLMessage</tt> in ein
	 * <tt>TargetSearchResult</tt> zurück
	 * 
	 * @param msg
	 *            Die <tt>ACLMessage</tt> mit dem relevanten Content
	 * @return Ein <tt>TargetSearchResult</tt>, wenn der Messagecontent
	 *         tatsächlich ein <tt>TargetSearchResult</tt> war. Falls der
	 *         Content ein anderes Datenformat hatte, wird <b>null</b>
	 *         zurückgegeben.
	 */
	public TargetSearchResult extractTargetSearchResultFromMsg(ACLMessage msg) {

		// Diese Attribute benötigen wir zum Rückwärtskonvertieren aus dem
		// Content String in eine Java Klasse
		ContentElement contentElement = null;
		TargetData td = null;
		TargetSearchResult result = null;

		try {

			// zunächst aus dem SL String ein allgemeines ContentElement
			// instantiieren
			contentElement = getContentManager().extractContent(msg);
			if (contentElement instanceof TargetData) {

				// den TargetData Container casten
				td = (TargetData) contentElement;
				// Das TargetScanResult aus dem TargetData Container
				// herausziehen:
				result = td.getTarget();
			} else {
				return null;
			}
		} catch (CodecException ce) {
			ce.printStackTrace();
		} catch (OntologyException oe) {
			oe.printStackTrace();
		}

		return result;
	}

	/**
	 * Konvertiert den Content einer <tt>ACLMessage</tt> in ein
	 * <tt>EnvironmentScanResult</tt> zurück
	 * 
	 * @param msg
	 *            Die <tt>ACLMessage</tt> mit dem relevanten Content
	 * @return Ein <tt>EnvironmentScanResult</tt>, wenn der Messagecontent
	 *         tatsächlich ein <tt>EnvironmentScanResult</tt> war. Falls der
	 *         Content ein anderes Datenformat hatte, wird <b>null</b>
	 *         zurückgegeben.
	 */
	public EnvironmentScanResult extractEnvironmentScanResultFromMsg(ACLMessage msg) {

		// Diese Attribute benötigen wir zum Rückwärtskonvertieren aus dem
		// Content String in eine Java Klasse
		ContentElement contentElement = null;
		EnvironmentData ed = null;
		EnvironmentScanResult result = null;

		try {

			// zunächst aus dem SL String ein allgemeines ContentElement
			// instantiieren
			contentElement = getContentManager().extractContent(msg);
			if (contentElement instanceof EnvironmentData) {

				// den EnvironmentData Container casten
				ed = (EnvironmentData) contentElement;
				// Das EnvironmentScanResult aus dem EnvironmentData Container
				// herausziehen:
				result = ed.getObstacles();
			} else {
				return null;
			}
		} catch (CodecException ce) {
			ce.printStackTrace();
		} catch (OntologyException oe) {
			oe.printStackTrace();
		}

		return result;
	}

	protected String extendLogMsg(String message) {
		return this.getName() + ": " + message;
	}

	public void printLn(String message) {
		System.out.println(extendLogMsg(message));
	}

	public void logDebug(String message) {
		logger.log(Logger.INFO, extendLogMsg(message));
	}

	public void logWarning(String message) {
		logger.log(Logger.WARNING, extendLogMsg(message));
	}

	public void logInfo(String message) {
		logger.log(Logger.INFO, extendLogMsg(message));
	}

}
